﻿using System;
using System.Collections.Generic;
using MLSharp.Utilities;

namespace MLSharp.Filters
{
	/// <summary>
	/// A filter that can normalize/standardize input variables of a data set.
	/// </summary>
	public class StandardizeFilter : IRequiresInitializationFilter
	{
		#region Private Fields

		/// <summary>
		/// The steps that must be executed to standardize a data set.
		/// </summary>
		private List<StandardizationStepBase> mStandardizeSteps;

		#endregion

		#region Public Constructors

		/// <summary>
		/// Creates a filter that will normalize data sets based on statistics computed
		/// from the specified data set.
		/// </summary>
		/// <param name="dataSet"></param>
		public StandardizeFilter(IDataSet dataSet)
		{
			Initialize(dataSet);
		}

		/// <summary>
		/// Creates an uninitialized filter.
		/// </summary>
		public StandardizeFilter()
		{
		}

		#endregion

		#region Private Methods

		/// <summary>
		/// Calculates the steps that must be taken to standardize the data.
		/// </summary>
		/// <param name="dataSet"></param>
		private void CalculateStandardizationSteps(IDataSet dataSet)
		{
			mStandardizeSteps = new List<StandardizationStepBase>();

			for (int i=0; i < dataSet.Attributes.Count; i++)
			{
				StandardizationStepBase step;

				switch (dataSet.Attributes[i].Type)
				{
					case AttributeType.Continuous:
					case AttributeType.Discrete:
						step = GetContinuousStep(i, dataSet);
						break;

					case AttributeType.Set:
						//Binary attributes are handled differently from regular set attributes
						step = (dataSet.Attributes[i].PossibleValues.Length <= 2) ? GetBinaryStep(i, dataSet) : GetSetStep(i, dataSet);
						break;

					default:
						throw new NotImplementedException();
				}

				if (dataSet.TargetAttributeIndex == i)
				{
					step.IsTarget = true;
				}
				
				step.AttributeType = dataSet.Attributes[i].Type;
				step.AttributeName = dataSet.Attributes[i].Name;

				mStandardizeSteps.Add(step);
			}
		}

		/// <summary>
		/// Iterates through the values for the specified attribute and calculates the range
		/// and midrange.
		/// </summary>
		/// <param name="dataSet">The data set.</param>
		/// <param name="attributeIndex">The attribute index.</param>
		/// <param name="midRange">The calculated midrange.</param>
		/// <param name="range">The calculated range.</param>
		private static void GetRangeAndMidRange(IDataSet dataSet, int attributeIndex, out double midRange, out double range)
		{
			double min = double.NaN, max = double.NaN;

			for (int i = 0; i < dataSet.Instances.Count; i++)
			{
				double value;

				//If the value is missing, skip it.
				if (dataSet.Instances[i].IsValueMissing(attributeIndex))
				{
					continue;
				}
				else
				{
					value = dataSet.Instances[i].Values[attributeIndex];
				}

				//If the value is the new max
				if (double.IsNaN(max) || value > max)
				{
					max = value;
				}

				//If the value is the new min
				if (double.IsNaN(min) || value < min)
				{
					min = value;
				}
			}

			midRange = (max + min) / 2.0;

			range = (max - min);
		}

		#endregion

		#region Protected Virtual Methods

		/// <summary>
		/// Gets a standardization step for a binary attribute.
		/// </summary>
		/// <param name="index"></param>
		/// <param name="dataSet"></param>
		/// <returns></returns>
		protected virtual StandardizationStepBase GetBinaryStep(int index, IDataSet dataSet)
		{
			BinaryStandardizationStep step = new BinaryStandardizationStep
			                                 	{
			                                 		PossibleValues = dataSet.Attributes[index].PossibleValues
			                                 	};

			return step;
		}

		/// <summary>
		/// Gets a standardization step for a set attribute with more 
		/// than two possible values.
		/// </summary>
		/// <param name="index"></param>
		/// <param name="dataSet"></param>
		/// <returns></returns>
		protected virtual StandardizationStepBase GetSetStep(int index, IDataSet dataSet)
		{
			//We will use the 1-of-C encoding, which converts the single set attribute
			//into sizeof(Set) attributes.
			SetStandardizationStep step = new SetStandardizationStep
			       	{
			       		PossibleValues = dataSet.Attributes[index].PossibleValues
			       	};

			//If this is the target attribute but has more than two possible values,
			//we can't standardize it.
			if (dataSet.TargetAttributeIndex == index && dataSet.Attributes[index].PossibleValues.Length > 2)
			{
				throw new NotSupportedException("Target attribute has more than two possible values; cannot be standardized (yet).");
			}

			return step;
		}

		/// <summary>
		/// Gets a step for standardizing a continuous or discreete attribute.
		/// </summary>
		/// <param name="index"></param>
		/// <param name="dataSet"></param>
		/// <returns></returns>
		protected virtual StandardizationStepBase GetContinuousStep(int index, IDataSet dataSet)
		{
			//Continuous and discrete attributes are standardized to
			//midrange 0 range 2 (ie: [-1,1]).
			double midRange, range;

			GetRangeAndMidRange(dataSet, index, out midRange, out range);

			ContinuousStandardizationStep step = new ContinuousStandardizationStep
			       	{
			       		MidRange = midRange,
			       		Range = range
			       	};

			return step;
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Standardizes the input values in the specified data set.
		/// </summary>
		/// <param name="dataSet">The data set.</param>
		/// <remarks>
		/// The data set must have the same attributes, types, etc. as
		/// the data set originally used to construct the filter.
		/// </remarks>
		public void Transform(IDataSet dataSet)
		{
			//This can be expensive in terms of memory usage, so force a GC
			//to free up as much memory as possible.
			GC.Collect();

			//This transformation will build a new data set that will eventually
			//replace the values of the specified data set.
			BasicDataSet stdDataSet = new BasicDataSet();

			//Build the standardized list of attributes.  The number and types of attributes may
			//change as things are recoded.
			foreach (StandardizationStepBase step in mStandardizeSteps)
			{
				if (step.AttributeType == AttributeType.Continuous ||
						 step.AttributeType == AttributeType.Discrete)
				{
					stdDataSet.Attributes.Add(DataAttribute.NewContinuousAttribute(step.AttributeName));
				}
				else if (step.AttributeType == AttributeType.Set)
				{
					//Only change the attribute name if we need to.  If there's just 2 possible values,
					//we can leave the attribute name unchanged.
					if (((SetStandardizationStep)step).PossibleValues.Length <= 2)
					{
						stdDataSet.Attributes.Add(DataAttribute.NewSetAttribute(step.AttributeName, "-1.0", "1.0"));
					}
					else
					{
						for (int i = 0; i < ((SetStandardizationStep)step).PossibleValues.Length - 1; i++)
						{
							stdDataSet.Attributes.Add(DataAttribute.NewSetAttribute(step.AttributeName + "_" + i, "-1.0", "1.0"));
						}
					}
				}

				//If this step's attribute is the target update the data set.
				if (step.IsTarget)
				{
					stdDataSet.TargetAttributeIndex = stdDataSet.Attributes.Count - 1;
				}
			}

			foreach (Instance instance in dataSet.Instances)
			{
				List<double> values = new List<double>();
				for (int i=0; i < instance.Values.Length; i++)
				{
					values.AddRange(mStandardizeSteps[i].Apply(instance.Values[i]));
				}

				Instance stdInstance = new Instance(instance.Label, values.ToArray());

				stdDataSet.Instances.Add(stdInstance);
			}

			dataSet.Attributes.Clear();
			dataSet.Attributes.AddRange(stdDataSet.Attributes);
			dataSet.Instances.Clear();
			//Move the instance to the new dataSet...
			while (stdDataSet.Instances.Count > 0)
			{
				Instance instance = stdDataSet.Instances[0];
				stdDataSet.Instances.RemoveAt(0);

				dataSet.Instances.Add(instance.Clone());
			}
			dataSet.TargetAttributeIndex = stdDataSet.TargetAttributeIndex;
		}

		/// <summary>
		/// Calculates the standardization steps using the specified datset..
		/// </summary>
		/// <param name="dataSet"></param>
		public void Initialize(IDataSet dataSet)
		{
			//This can be expensive in terms of memory usage, so force a GC
			//to free up as much memory as possible.
			GC.Collect();

			CalculateStandardizationSteps(dataSet);
		}

		#endregion

		#region Internal Classes

		/// <summary>
		/// Provides shared members to all types of standardization steps.
		/// </summary>
		protected abstract class StandardizationStepBase
		{
			/// <summary>
			/// The type of the original attribute.
			/// </summary>
			public AttributeType AttributeType { get; set; }

			/// <summary>
			/// The name of the original attribute.
			/// </summary>
			public string AttributeName { get; set; }

			/// <summary>
			/// True if the step's attribute is the target attribute.
			/// </summary>
			public bool IsTarget { get; set; }

			/// <summary>
			/// True if the step's attribute is the label attribute.
			/// </summary>
			public bool IsLabel { get; set; }

			/// <summary>
			/// Applies the standardization to the specified value.
			/// </summary>
			/// <param name="value"></param>
			/// <returns></returns>
			public abstract double[] Apply(double value);
		}

		/// <summary>
		/// Stores information about a step that must be taken to 
		/// standardize an attribute of a data set.
		/// </summary>
		protected class ContinuousStandardizationStep : StandardizationStepBase
		{
			/// <summary>
			/// The midrange of the attribute values.
			/// </summary>
			public double MidRange { get; set; }

			/// <summary>
			/// The range of the attribute values.
			/// </summary>
			public double Range { get; set; }

			/// <summary>
			/// Applies the standardization to the specified value.
			/// </summary>
			/// <param name="value"></param>
			/// <returns></returns>
			public override double[] Apply(double value)
			{
				if (double.IsNaN(value))
				{
					return new[] { double.NaN };
				}

				value = (value - MidRange) / (Range / 2.0);

				return new[] { value };
			}
		}

		/// <summary>
		/// Stores the information needed to standardize a Set-based attribute.
		/// </summary>
		protected class SetStandardizationStep : StandardizationStepBase
		{
			/// <summary>
			/// Stores the possible values for this attribute.
			/// </summary>
			public string[] PossibleValues { get; set; }

			/// <summary>
			/// Applies the standardization to the specified value.
			/// </summary>
			/// <param name="index"></param>
			/// <returns></returns>
			public override double[] Apply(double index)
			{
				//We are using 1-of-(C-1) encoding.
				double[] values = new double[PossibleValues.Length - 1];

				if (index < 0)
				{
					for (int i =0; i < values.Length; i++)
					{
						values[i] = double.NaN;
					}

					return values;
				}

				for (int i = 0; i < PossibleValues.Length - 1; i++)
				{
					//We are setting the *index* here, so if 
					values[i] = (i == index ? 1 : 0);
				}

				return values;
			}
		}

		/// <summary>
		/// A step that indicates a binary attribute.
		/// </summary>
		protected class BinaryStandardizationStep : SetStandardizationStep
		{
			/// <summary>
			/// Applies the standardization to the specified value.
			/// </summary>
			/// <param name="index"></param>
			/// <returns></returns>
			public override double[] Apply(double index)
			{
				if (double.IsNaN(index) || index < 0)
				{
					return new [] { double.NaN };
				}
				else
				{
					return new[] { index };
				}
			}
		}

		#endregion
	}
}
